package com.example.datajdbc.ext.spring.data.jdbc;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.ibatis.session.SqlSession;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.data.jdbc.core.convert.EntityRowMapper;
import org.springframework.data.jdbc.core.convert.JdbcConverter;
import org.springframework.data.jdbc.repository.QueryMappingConfiguration;
import org.springframework.data.jdbc.repository.query.JdbcQueryMethod;
import org.springframework.data.jdbc.repository.query.PartTreeJdbcQuery;
import org.springframework.data.jdbc.repository.query.StringBasedJdbcQuery;
import org.springframework.data.mapping.callback.EntityCallbacks;
import org.springframework.data.projection.ProjectionFactory;
import org.springframework.data.relational.core.dialect.Dialect;
import org.springframework.data.relational.core.mapping.RelationalMappingContext;
import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;
import org.springframework.data.relational.core.mapping.event.AfterConvertCallback;
import org.springframework.data.relational.core.mapping.event.AfterConvertEvent;
import org.springframework.data.repository.core.NamedQueries;
import org.springframework.data.repository.core.RepositoryMetadata;
import org.springframework.data.repository.query.QueryLookupStrategy;
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
import org.springframework.data.repository.query.RepositoryQuery;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.SingleColumnRowMapper;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;

import java.lang.reflect.Method;
import java.sql.ResultSet;
import java.sql.SQLException;

/**
 * 这个类修改了 {@link DeclaredQueryLookupStrategy} 查找 RepositoryQuery 的处理
 * 首先按 Data Jdbc 的逻辑创建基于 NamedParameterJdbcOperations 处理参数并执行的 StringBasedJdbcQuery
 * 如果找不到说明未使用 Data Jdbc 方式，然后根据接口类型获取 Mybatis Mapper 动态代理实例创建基于动态代理实例执行的 MybatisQuery
 *
 * @see org.springframework.data.jdbc.repository.support.JdbcQueryLookupStrategy.DeclaredQueryLookupStrategy
 */
public abstract class MybatisQueryLookupStrategy implements QueryLookupStrategy {

    private static final Log LOG = LogFactory.getLog(MybatisQueryLookupStrategy.class);

    private final ApplicationEventPublisher publisher;
    @Nullable
    private final EntityCallbacks callbacks;
    private final RelationalMappingContext context;
    private final JdbcConverter converter;
    private final Dialect dialect;
    private final QueryMappingConfiguration queryMappingConfiguration;
    private final NamedParameterJdbcOperations operations;
    @Nullable
    private final BeanFactory beanfactory;
    protected final QueryMethodEvaluationContextProvider evaluationContextProvider;

    MybatisQueryLookupStrategy(final ApplicationEventPublisher publisher, @Nullable final EntityCallbacks callbacks,
            final RelationalMappingContext context, final JdbcConverter converter, final Dialect dialect,
            final QueryMappingConfiguration queryMappingConfiguration, final NamedParameterJdbcOperations operations,
            @Nullable final BeanFactory beanfactory,
            final QueryMethodEvaluationContextProvider evaluationContextProvider) {

        Assert.notNull(publisher, "ApplicationEventPublisher must not be null");
        Assert.notNull(context, "RelationalMappingContextPublisher must not be null");
        Assert.notNull(converter, "JdbcConverter must not be null");
        Assert.notNull(dialect, "Dialect must not be null");
        Assert.notNull(queryMappingConfiguration, "QueryMappingConfiguration must not be null");
        Assert.notNull(operations, "NamedParameterJdbcOperations must not be null");
        Assert.notNull(evaluationContextProvider, "QueryMethodEvaluationContextProvier must not be null");

        this.publisher = publisher;
        this.callbacks = callbacks;
        this.context = context;
        this.converter = converter;
        this.dialect = dialect;
        this.queryMappingConfiguration = queryMappingConfiguration;
        this.operations = operations;
        this.beanfactory = beanfactory;
        this.evaluationContextProvider = evaluationContextProvider;
    }

    static class CreateQueryLookupStrategy extends MybatisQueryLookupStrategy {

        CreateQueryLookupStrategy(final ApplicationEventPublisher publisher, @Nullable final EntityCallbacks callbacks,
                final RelationalMappingContext context, final JdbcConverter converter, final Dialect dialect,
                final QueryMappingConfiguration queryMappingConfiguration,
                final NamedParameterJdbcOperations operations,
                @Nullable final BeanFactory beanfactory,
                final QueryMethodEvaluationContextProvider evaluationContextProvider) {

            super(publisher, callbacks, context, converter, dialect, queryMappingConfiguration, operations, beanfactory,
                    evaluationContextProvider);
        }

        @Override
        public RepositoryQuery resolveQuery(final Method method, final RepositoryMetadata repositoryMetadata,
                final ProjectionFactory projectionFactory, final NamedQueries namedQueries) {

            final JdbcQueryMethod queryMethod = this.getJdbcQueryMethod(method, repositoryMetadata, projectionFactory,
                    namedQueries);

            return new PartTreeJdbcQuery(this.getContext(), queryMethod, this.getDialect(), this.getConverter(),
                    this.getOperations(),
                    this::createMapper);
        }
    }

    static class DeclaredQueryLookupStrategy extends MybatisQueryLookupStrategy {

        private final SqlSession session;

        DeclaredQueryLookupStrategy(final ApplicationEventPublisher publisher,
                @Nullable final EntityCallbacks callbacks,
                final RelationalMappingContext context, final JdbcConverter converter, final Dialect dialect,
                final QueryMappingConfiguration queryMappingConfiguration,
                final NamedParameterJdbcOperations operations, final SqlSession session,
                @Nullable final BeanFactory beanfactory,
                final QueryMethodEvaluationContextProvider evaluationContextProvider) {
            super(publisher, callbacks, context, converter, dialect, queryMappingConfiguration, operations, beanfactory,
                    evaluationContextProvider);

            Assert.notNull(session, "SqlSession must not be null");

            this.session = session;
        }

        @Override
        public RepositoryQuery resolveQuery(final Method method, final RepositoryMetadata repositoryMetadata,
                final ProjectionFactory projectionFactory, final NamedQueries namedQueries) {

            final JdbcQueryMethod queryMethod = this.getJdbcQueryMethod(method, repositoryMetadata, projectionFactory,
                    namedQueries);

            if (namedQueries.hasQuery(queryMethod.getNamedQueryName()) || queryMethod.hasAnnotatedQuery()) {

                if (queryMethod.hasAnnotatedQuery() && queryMethod.hasAnnotatedQueryName()) {
                    LOG.warn(String.format(
                            "Query method %s is annotated with both, a query and a query name; Using the declared query",
                            method));
                }

                final StringBasedJdbcQuery query = new StringBasedJdbcQuery(queryMethod, this.getOperations(),
                        this::createMapper,
                        this.getConverter(), this.evaluationContextProvider);
                query.setBeanFactory(this.getBeanFactory());
                return query;
            }

            final Object mybatisMapper = this.session.getMapper(method.getDeclaringClass());
            if (mybatisMapper != null) {
                return new MybatisQuery(method, queryMethod, mybatisMapper);
            }

            throw new IllegalStateException(
                    String.format("Did neither find a NamedQuery nor an annotated query for method %s", method));
        }
    }

    static class CreateIfNotFoundQueryLookupStrategy extends MybatisQueryLookupStrategy {

        private final DeclaredQueryLookupStrategy lookupStrategy;
        private final CreateQueryLookupStrategy createStrategy;

        CreateIfNotFoundQueryLookupStrategy(final ApplicationEventPublisher publisher,
                @Nullable final EntityCallbacks callbacks,
                final RelationalMappingContext context, final JdbcConverter converter, final Dialect dialect,
                final QueryMappingConfiguration queryMappingConfiguration,
                final NamedParameterJdbcOperations operations,
                @Nullable final BeanFactory beanfactory, final CreateQueryLookupStrategy createStrategy,
                final DeclaredQueryLookupStrategy lookupStrategy,
                final QueryMethodEvaluationContextProvider evaluationContextProvider) {

            super(publisher, callbacks, context, converter, dialect, queryMappingConfiguration, operations, beanfactory,
                    evaluationContextProvider);

            Assert.notNull(createStrategy, "CreateQueryLookupStrategy must not be null");
            Assert.notNull(lookupStrategy, "DeclaredQueryLookupStrategy must not be null");

            this.createStrategy = createStrategy;
            this.lookupStrategy = lookupStrategy;
        }

        @Override
        public RepositoryQuery resolveQuery(final Method method, final RepositoryMetadata repositoryMetadata,
                final ProjectionFactory projectionFactory, final NamedQueries namedQueries) {

            try {
                return this.lookupStrategy.resolveQuery(method, repositoryMetadata, projectionFactory, namedQueries);
            } catch (final IllegalStateException e) {
                return this.createStrategy.resolveQuery(method, repositoryMetadata, projectionFactory, namedQueries);
            }
        }
    }

    JdbcQueryMethod getJdbcQueryMethod(final Method method, final RepositoryMetadata repositoryMetadata,
            final ProjectionFactory projectionFactory, final NamedQueries namedQueries) {
        return new JdbcQueryMethod(method, repositoryMetadata, projectionFactory, namedQueries, this.context);
    }

    public static QueryLookupStrategy create(@Nullable final Key key, final ApplicationEventPublisher publisher,
            @Nullable final EntityCallbacks callbacks, final RelationalMappingContext context,
            final JdbcConverter converter, final Dialect dialect,
            final QueryMappingConfiguration queryMappingConfiguration, final NamedParameterJdbcOperations operations,
            final SqlSession session,
            @Nullable final BeanFactory beanFactory,
            final QueryMethodEvaluationContextProvider evaluationContextProvider) {

        Assert.notNull(publisher, "ApplicationEventPublisher must not be null");
        Assert.notNull(context, "RelationalMappingContextPublisher must not be null");
        Assert.notNull(converter, "JdbcConverter must not be null");
        Assert.notNull(dialect, "Dialect must not be null");
        Assert.notNull(queryMappingConfiguration, "QueryMappingConfiguration must not be null");
        Assert.notNull(operations, "NamedParameterJdbcOperations must not be null");
        Assert.notNull(operations, "SqlSession must not be null");

        final CreateQueryLookupStrategy createQueryLookupStrategy = new CreateQueryLookupStrategy(publisher, callbacks,
                context,
                converter, dialect, queryMappingConfiguration, operations, beanFactory, evaluationContextProvider);

        final DeclaredQueryLookupStrategy declaredQueryLookupStrategy = new DeclaredQueryLookupStrategy(publisher,
                callbacks,
                context, converter, dialect, queryMappingConfiguration, operations, session, beanFactory,
                evaluationContextProvider);

        final Key cleanedKey = key != null ? key : Key.CREATE_IF_NOT_FOUND;

        LOG.debug(String.format("Using the queryLookupStrategy %s", cleanedKey));

        switch (cleanedKey) {
            case CREATE:
                return createQueryLookupStrategy;
            case USE_DECLARED_QUERY:
                return declaredQueryLookupStrategy;
            case CREATE_IF_NOT_FOUND:
                return new CreateIfNotFoundQueryLookupStrategy(publisher, callbacks, context, converter, dialect,
                        queryMappingConfiguration, operations, beanFactory, createQueryLookupStrategy,
                        declaredQueryLookupStrategy,
                        evaluationContextProvider);
            default:
                throw new IllegalArgumentException(String.format("Unsupported query lookup strategy %s", key));
        }
    }

    RelationalMappingContext getContext() {
        return this.context;
    }

    JdbcConverter getConverter() {
        return this.converter;
    }

    Dialect getDialect() {
        return this.dialect;
    }

    NamedParameterJdbcOperations getOperations() {
        return this.operations;
    }

    @Nullable
    BeanFactory getBeanFactory() {
        return this.beanfactory;
    }

    @SuppressWarnings("unchecked")
    RowMapper<Object> createMapper(final Class<?> returnedObjectType) {

        final RelationalPersistentEntity<?> persistentEntity = this.context.getPersistentEntity(returnedObjectType);

        if (persistentEntity == null) {
            return (RowMapper<Object>) SingleColumnRowMapper.newInstance(returnedObjectType,
                    this.converter.getConversionService());
        }

        return (RowMapper<Object>) this.determineDefaultMapper(returnedObjectType);
    }

    private RowMapper<?> determineDefaultMapper(final Class<?> returnedObjectType) {

        final RowMapper<?> configuredQueryMapper = this.queryMappingConfiguration.getRowMapper(returnedObjectType);

        if (configuredQueryMapper != null)
            return configuredQueryMapper;

        final EntityRowMapper<?> defaultEntityRowMapper = new EntityRowMapper<>( //
                this.context.getRequiredPersistentEntity(returnedObjectType), //
                this.converter //
        );

        return new PostProcessingRowMapper<>(defaultEntityRowMapper);
    }

    class PostProcessingRowMapper<T> implements RowMapper<T> {

        private final RowMapper<T> delegate;

        PostProcessingRowMapper(final RowMapper<T> delegate) {
            this.delegate = delegate;
        }

        @Override
        public T mapRow(final ResultSet rs, final int rowNum) throws SQLException {

            final T entity = this.delegate.mapRow(rs, rowNum);

            if (entity != null) {

                MybatisQueryLookupStrategy.this.publisher.publishEvent(new AfterConvertEvent<>(entity));

                if (MybatisQueryLookupStrategy.this.callbacks != null) {
                    return MybatisQueryLookupStrategy.this.callbacks.callback(AfterConvertCallback.class, entity);
                }
            }

            return entity;
        }
    }
}
